<?php

declare(strict_types=1);

namespace DreamCat\ControllerParamGen\Generator;

use Components\Utils\Funcs\ArrayFunc;
use DreamCat\Array2Class\Array2ClassInterface;
use DreamCat\Array2Class\Exception\ConvertFailed;
use DreamCat\ControllerParamGen\Annotation\CtlParamType;
use DreamCat\ControllerParamGen\ControllerParamGeneratorInterface;
use DreamCat\ControllerParamGen\Decoder\FormBodyDecoder;
use DreamCat\ControllerParamGen\Decoder\JsonBodyDecoder;
use DreamCat\ControllerParamGen\Exception\ParamGenerateFailed;
use DreamCat\ControllerParamGen\HttpBodyDecodeInterface;
use DreamCat\ControllerParamGen\ParamCreateInterface;
use DreamCat\ObjectValid\Utils\AnnotationUtils;
use DreamCat\ObjectValid\ValidationInterface;
use DreamCat\ObjectValid\Validor\Validator;
use Dreamcat\PropertyAnalysis\Pojo\BuildinType;
use Dreamcat\PropertyAnalysis\Utils\BuildinTypeHelper;
use JetBrains\PhpStorm\Pure;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
use ReflectionClass;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionUnionType;
use Throwable;

/**
 * 控制器参数创建器
 * @author vijay
 */
class ControllerParamGenerator implements ControllerParamGeneratorInterface
{
    /** @var ParamCreateInterface[] 特殊的参数创建接口 */
    private array $specParams = [];
    /** @var HttpBodyDecodeInterface[] http解码器 */
    private array $decoders = [];
    /** @var ValidationInterface 参数校验器 */
    private ValidationInterface $validation;
    /** @var string 忽略检查参数的注解 */
    private string $ignoreAnno = "";

    /**
     * @param ContainerInterface $container 容器接口
     * @param Array2ClassInterface $array2Class 数组转类的接口
     */
    #[Pure]
    public function __construct(private ContainerInterface $container, private Array2ClassInterface $array2Class)
    {
        $this->decoders[] = new JsonBodyDecoder();
        $this->decoders[] = new FormBodyDecoder();
    }

    /**
     * 设置忽略检查参数的注解
     * @param string $ignoreAnno
     * @return void
     */
    public function setIgnoreAnno(string $ignoreAnno): void
    {
        $this->ignoreAnno = $ignoreAnno;
    }


    /** @inheritDoc */
    public function generateParams(
        ReflectionMethod $reflectionMethod,
        ServerRequestInterface $serverRequest,
        array $pathVariables
    ): array {
        $result = [];
        foreach ($reflectionMethod->getParameters() as $parameter) {
            $param = $this->generateParam($parameter, $serverRequest, $pathVariables);
            if (is_null($param) && !$parameter->allowsNull()) {
                throw new ParamGenerateFailed("{$reflectionMethod->getName()}::{$parameter->getName()} 参数不为能null");
            }
            if (!$this->ignoreAnno || !AnnotationUtils::getAttrObj($parameter, $this->ignoreAnno)) {
                $this->getValidation()->validParam($parameter, $param);
            }
            $result[] = $param;
        }
        return $result;
    }

    /**
     * 生成一个参数
     * @param ReflectionParameter $parameter 参数反射
     * @param ServerRequestInterface $serverRequest 请求对象
     * @param array $pathVariables 占位符列表
     * @return mixed 参数
     */
    protected function generateParam(
        ReflectionParameter $parameter,
        ServerRequestInterface $serverRequest,
        array $pathVariables
    ): mixed {
        $ctlType = $parameter->getAttributes(CtlParamType::class);
        if ($ctlType) {
            /** @var CtlParamType $ctlType */
            $ctlType = $ctlType[0]->newInstance();
        } else {
            $ctlType = null;
        }
        return match ($ctlType?->getType()) {
            CtlParamType::TYPE_REQUEST_BODY => $this->assignRequestBody(
                $parameter,
                $serverRequest,
                $ctlType->getPath()
            ),
            CtlParamType::TYPE_PATH_VARIABLE => $this->assignArrayVariable(
                $parameter,
                $serverRequest,
                $pathVariables,
                $ctlType->getPath(),
                false
            ),
            CtlParamType::TYPE_GET_PARAM => $this->assignArrayVariable(
                $parameter,
                $serverRequest,
                $serverRequest->getQueryParams(),
                $ctlType->getPath(),
                false
            ),
            CtlParamType::TYPE_HEADER => $this->assignHeaderVariable(
                $parameter,
                $serverRequest,
                $serverRequest->getHeaders(),
                $ctlType->getPath()
            ),
            CtlParamType::TYPE_COOKIE => $this->assignArrayVariable(
                $parameter,
                $serverRequest,
                $serverRequest->getCookieParams(),
                $ctlType->getPath(),
                false
            ),
            default => $this->autoValue($parameter, $pathVariables, $serverRequest),
        };
    }

    /**
     * 无注解时从请求中获取数据
     * @param ReflectionParameter $parameter 函数参数反射
     * @param array $pathVariables 占位符列表
     * @param ServerRequestInterface $serverRequest http请求参数
     * @return mixed
     */
    protected function autoValue(
        ReflectionParameter $parameter,
        array $pathVariables,
        ServerRequestInterface $serverRequest
    ): mixed {
        # 先尝试在占位符查找
        if (isset($pathVariables[$parameter->name])) {
            return $this->assignArrayVariable($parameter, $serverRequest, $pathVariables, $parameter->name);
        }
        # 再尝试在GET中查询
        $get = $serverRequest->getQueryParams();
        if (isset($get[$parameter->name])) {
            return $this->assignArrayVariable($parameter, $serverRequest, $get, $parameter->name);
        }
        return $this->dealNotFound($parameter, $serverRequest);
    }

    /**
     * 根据报头从请求中解析参数
     * @param ReflectionParameter $parameter 函数参数信息
     * @param ServerRequestInterface $serverRequest 请求对象
     * @param string[][] $headers 报头数组
     * @param string|null $name 信息是数组时表示字段名称
     * @return mixed
     */
    protected function assignHeaderVariable(
        ReflectionParameter $parameter,
        ServerRequestInterface $serverRequest,
        array $headers,
        ?string $name
    ): mixed {
        $arrays = [];
        foreach ($headers as $headName => $headerAry) {
            $cnt = count($headerAry);
            $arrays[$headName] = match ($cnt) {
                1 => $headerAry[0],
                default => $headerAry
            };
        }
        return $this->assignArrayVariable($parameter, $serverRequest, $arrays, $name, false);
    }

    /**
     * 根据 PathVariable 或 GetParam 注解从请求中获取数据
     * @param ReflectionParameter $parameter 函数参数信息
     * @param ServerRequestInterface $serverRequest 请求对象
     * @param array $arrayVariables 数据数组
     * @param string|null $name 信息是数组时表示字段名称
     * @param bool $dealNotFound 是否尝试自动判断
     * @return mixed
     */
    protected function assignArrayVariable(
        ReflectionParameter $parameter,
        ServerRequestInterface $serverRequest,
        array $arrayVariables,
        ?string $name,
        bool $dealNotFound = true
    ): mixed {
        if ($name !== null) {
            $arrayVariables = ArrayFunc::getArrayChild($arrayVariables, $name);
        } elseif ($parameter->getType() instanceof ReflectionNamedType) {
            $type = $parameter->getType()->getName();
            if (BuildinTypeHelper::IS_SCALAR[$type] ?? false) {
                $arrayVariables = ArrayFunc::getArrayChild($arrayVariables, $parameter->name);
            }
        }
        if ($arrayVariables === null) {
            return $this->dealNotFound($parameter, $serverRequest);
        }
        return $this->assignClassProperty($parameter, $serverRequest, $arrayVariables, $dealNotFound);
    }

    /**
     * 根据RequestBody注解从请求中获取数据
     * @param ReflectionParameter $parameter 函数参数信息
     * @param ServerRequestInterface $serverRequest 请求对象
     * @param ?string $name 信息是数组时表示字段名称
     * @return mixed
     */
    protected function assignRequestBody(
        ReflectionParameter $parameter,
        ServerRequestInterface $serverRequest,
        ?string $name
    ): mixed {
        $requestBody = strval($serverRequest->getBody());
        if ($requestBody == "") {
            return $this->dealNotFound($parameter, $serverRequest);
        }
        $type = $parameter->getType();
        if ($type instanceof ReflectionNamedType && $type->getName() == BuildinType::STRING && $name === null) {
            return $requestBody;
        }
        $body = $this->getParsedBody(
            $serverRequest->getHeader("content-type")[0] ?? "",
            $requestBody,
            $serverRequest->getParsedBody()
        );
        if ($name !== null && is_array($body)) {
            $body = ArrayFunc::getArrayChild($body, $name);
        }
        return $this->assignClassProperty($parameter, $serverRequest, $body, false);
    }

    /**
     * 获取解析后的消息体
     * @param string $contentType content-type
     * @param string $requestBody http请求信息消息体
     * @param object|array|null $parsedBody 解析后的数据
     * @return mixed 解析后的数据数组化
     */
    protected function getParsedBody(string $contentType, string $requestBody, object|array|null $parsedBody): mixed
    {
        if ($parsedBody) {
            return (array)$parsedBody;
        }
        $contentType = strtolower($contentType);
        foreach ($this->getDecoders() as $decoder) {
            if ($decoder->match($contentType)) {
                return $decoder->parseBody($requestBody);
            }
        }
        throw new ParamGenerateFailed("无法解析消息体, type = {$contentType}");
    }

    /** @noinspection PhpDocMissingThrowsInspection */
    /**
     * 在找不到数据时的默认处理
     * @param ReflectionParameter $parameter 函数参数信息
     * @param ServerRequestInterface $serverRequest 请求对象
     * @return mixed
     */
    protected function dealNotFound(ReflectionParameter $parameter, ServerRequestInterface $serverRequest): mixed
    {
        if ($parameter->isDefaultValueAvailable()) {
            /** @noinspection PhpUnhandledExceptionInspection */
            return $parameter->getDefaultValue();
        }
        $paramTypes = $parameter->getType();
        if (!$paramTypes) {
            $paramTypes = [];
        } elseif ($paramTypes instanceof ReflectionUnionType) {
            $paramTypes = $paramTypes->getTypes();
        } else {
            $paramTypes = [$paramTypes];
        }
        foreach ($paramTypes as $paramType) {
            $type = $paramType->getName();
            if (isset($this->specParams[$type])) {
                return $this->getSpecParam($type)->createParam($serverRequest, $type);
            } else {
                try {
                    return $this->getContainer()->get($type);
                } catch (Throwable) {
                }
            }
        }
        if ($parameter->allowsNull()) {
            return null;
        }
        foreach ($paramTypes as $paramType) {
            $type = $paramType->getName();
            if (isset(BuildinTypeHelper::DEFAULT_VALUE[$type])) {
                return BuildinTypeHelper::DEFAULT_VALUE[$type];
            } elseif (class_exists($type)) {
                $class = new ReflectionClass($type);
                $constructor = $class->getConstructor();
                if (!$constructor || $constructor->getNumberOfRequiredParameters() == 0) {
                    /** @noinspection PhpUnhandledExceptionInspection */
                    return $class->newInstance();
                }
            }
        }

        $ctl = $parameter->getDeclaringClass()->name;
        $method = $parameter->getDeclaringFunction()->name;
        $paramName = $parameter->name;
        throw new ParamGenerateFailed("无法构建 {$ctl}::{$method}::{$paramName} 参数");
    }

    /**
     * 使用转换器生成参数
     * @param ReflectionParameter $parameter 参数反射
     * @param ServerRequestInterface $serverRequest 请求对象
     * @param mixed $data 输入的数据
     * @param bool $dealNotFound 是否尝试自动判断
     * @return mixed 生成的参数
     */
    protected function assignClassProperty(
        ReflectionParameter $parameter,
        ServerRequestInterface $serverRequest,
        mixed $data,
        bool $dealNotFound = true
    ): mixed {
        if ($parameter->getType()) {
            try {
                return $this->getArray2Class()->convertByType(
                    $data,
                    $parameter->getType(),
                    $parameter->getAttributes()
                );
            } catch (ConvertFailed $convertFailed) {
                if ($dealNotFound) {
                    return $this->dealNotFound($parameter, $serverRequest);
                } else {
                    throw new ParamGenerateFailed(
                        $convertFailed->getMessage(),
                        $convertFailed->getCode(),
                        $convertFailed
                    );
                }
            }
        } else {
            return $data;
        }
    }

    /**
     * @return ContainerInterface 容器接口
     */
    #[Pure]
    public function getContainer(): ContainerInterface
    {
        return $this->container;
    }

    /**
     * @param ContainerInterface $container 容器接口
     */
    public function setContainer(ContainerInterface $container): void
    {
        $this->container = $container;
    }

    /**
     * 获取所有的特殊参数创建接口
     * @return ParamCreateInterface[] key是参数类型
     */
    #[Pure]
    public function getSpecParams(): array
    {
        return $this->specParams;
    }

    /**
     * 获取指定的参数创建接口
     * @param string $type 参数类型
     * @return ?ParamCreateInterface 不存在返回null，存在返回创建参数的接口
     */
    #[Pure]
    public function getSpecParam(string $type): ?ParamCreateInterface
    {
        return $this->specParams[$type] ?? null;
    }

    /**
     * 添加一个特殊的参数创建
     * @param string|string[] $types
     * @param ParamCreateInterface $paramCreate
     */
    public function addSpecParam(string|array $types, ParamCreateInterface $paramCreate): void
    {
        if (!is_array($types)) {
            $types = [$types];
        }
        foreach ($types as $type) {
            $this->specParams[$type] = $paramCreate;
        }
    }

    /**
     * @return Array2ClassInterface
     */
    #[Pure]
    public function getArray2Class(): Array2ClassInterface
    {
        return $this->array2Class;
    }

    /**
     * @param Array2ClassInterface $array2Class
     * @codeCoverageIgnore
     */
    public function setArray2Class(Array2ClassInterface $array2Class): void
    {
        $this->array2Class = $array2Class;
    }

    /**
     * 添加http消息体解码器
     * @param HttpBodyDecodeInterface $bodyDecoder 消息体解码器
     * @return void
     * @codeCoverageIgnore
     */
    public function addHttpBodyDecoder(HttpBodyDecodeInterface $bodyDecoder): void
    {
        $this->decoders[] = $bodyDecoder;
    }

    /**
     * @return HttpBodyDecodeInterface[]
     */
    #[Pure]
    public function getDecoders(): array
    {
        return $this->decoders;
    }

    /**
     * @return ValidationInterface 参数校验器
     */
    public function getValidation(): ValidationInterface
    {
        if (!isset($this->validation)) {
            $this->validation = new Validator();
        }
        return $this->validation;
    }

    /**
     * @param ValidationInterface $validation 参数校验器
     */
    public function setValidation(ValidationInterface $validation): void
    {
        $this->validation = $validation;
    }
}

# end of file
